Skip to content

fix: enforce one-issue-per-PR across miners in mirror issue discovery - #822

Merged
anderdc merged 2 commits into
entrius:testfrom
mkdev5:fix/mirror-cross-miner-one-issue-per-pr
May 1, 2026
Merged

fix: enforce one-issue-per-PR across miners in mirror issue discovery#822
anderdc merged 2 commits into
entrius:testfrom
mkdev5:fix/mirror-cross-miner-one-issue-per-pr

Conversation

@mkdev5

@mkdev5 mkdev5 commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

PR #796 inlined the one-issue-per-PR dedup set inside _score_miner_mirror_issues (called once per miner), shrinking the rule's lifetime from "round" to "single miner." A solving PR closing issues authored by multiple miners then paid each one — silently inflating discovery emissions on closes #N closes #M PRs and enabling two-account collusion.

This PR restores legacy round-global semantics by:

  1. Deferred scoring: run_mirror_issue_discovery collects every miner's (evaluation, filtered_issues, open_issue_count) before scoring, so the one-issue-per-PR decision sees all miners at once.
  2. Cross-miner canonical map: A new _build_canonical_pr_owners helper produces (repo, pr_number) → (created_at, issue_number, uid) for the earliest-created qualifying issue across all miners.
  3. Marker comparison replaces per-miner set: In _score_miner_mirror_issues, the per-miner pr_scored_keys set and its pr_key in pr_scored_keys / pr_scored_keys.add(pr_key) pair are replaced with an exact marker-tuple comparison against the canonical map.

Same-account issues (discoverer == solver) never claim the slot, mirroring the legacy pr_scored.add ordering. _FAR_FUTURE is hoisted to module scope so the canonical builder and scorer share it. The scorer's signature gains only canonical_pr_owners; its classification path and counters are unchanged. Public API, log shapes, and cache stats are unchanged — only which miner pockets the score on a shared PR changes.

Related Issues

Closes #821

Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Other (describe below)

Testing

  • Tests added/updated
  • Manually tested

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Changes are documented (if applicable)

@xiao-xiao-mao xiao-xiao-mao Bot added the bug Something isn't working label Apr 27, 2026
@mkdev5
mkdev5 force-pushed the fix/mirror-cross-miner-one-issue-per-pr branch from 844a15c to e460e02 Compare April 27, 2026 23:51
@mkdev5
mkdev5 force-pushed the fix/mirror-cross-miner-one-issue-per-pr branch from e460e02 to 46c20e5 Compare April 28, 2026 04:57

@anderdc anderdc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functionally correct, but more re-architecting than the bug needs. The minimum fix is: hoist the dedup decision to round scope. Everything else (the NamedTuple, the phase-1 partition) is incidental.

Concrete scope-down ask:

  1. Drop _MinerBatch. Phase 1 just collects pending: List[Tuple[MinerEvaluation, List[MirrorIssue], int]] (eval, filtered, open_issue_count) — same as today, minus the inline classify loop.

  2. _build_canonical_pr_owners(pending) classifies inline:

    for evaluation, filtered, _ in pending:
        for issue in filtered:
            if _classify_issue(issue) != 'solved':
                continue
            sp = issue.solving_pr
            if issue.author_github_id == sp.author_github_id:
                continue
            ...
    

    Yes, `_classify_issue` runs twice per issue (here + in the scorer). It's attribute checks — microseconds at miner scale. Not worth a NamedTuple.

  3. `_score_miner_mirror_issues` keeps its current signature and inline classification path. The only change is replacing `pr_scored_keys: Set[Tuple[str, int]] = set()` and the `pr_key in pr_scored_keys` check with the marker comparison against `canonical_pr_owners`.

  4. Pull `_FAR_FUTURE = datetime.max.replace(tzinfo=timezone.utc)` to module scope so the scorer and canonical builder share it instead of redefining.

Net diff should be ~50 lines instead of ~140, and the scorer barely changes — which is what you want for a scoring-economy fix where the diff itself is the audit surface.

Tests stay as-is; they target `_build_canonical_pr_owners` and end-to-end behavior, both unchanged by the refactor.

PR #796 inlined the one-issue-per-PR set inside _score_miner_mirror_issues,
making the rule per-miner rather than per-round. A solving PR closing issues
authored by multiple miners awarded discovery score to each miner — silently
inflating issue-discovery emissions on the common 'closes #N closes #M'
GitHub pattern and enabling cheap two-account collusion.

Restore legacy round-global semantics by deferring scoring until every
miner's batch is fetched, then resolving canonical (repo, pr_number)
ownership across miners via _build_canonical_pr_owners. The earliest-created
qualifying issue across all miners owns the slot; siblings on the same PR
(including a single miner's later issues) count for credibility only.
Same-account issues never claim the slot, mirroring legacy ordering.
@mkdev5
mkdev5 force-pushed the fix/mirror-cross-miner-one-issue-per-pr branch from 5c45d0c to 6e2e83e Compare May 1, 2026 19:19
@mkdev5
mkdev5 requested a review from anderdc May 1, 2026 19:20
@mkdev5

mkdev5 commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

@anderdc please review the update

@anderdc
anderdc merged commit d51c4de into entrius:test May 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Mirror issue discovery one-issue-per-PR rule is enforced per-miner rather than per-round

2 participants